package biz.datalk.industrialland.common.util;

import biz.datalk.commons.utils.BasicParserUtils;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;

/**
 * 文件下载工具类
 *
 * @author tarofang@163.com
 * @date 2021-08-05
 */
public class DownloadUtil {
    private static final Logger log = LoggerFactory.getLogger(DownloadUtil.class);

    /** "Content-Range"格式是 "bytes <start>-<end>/<total>"，其中start和end必需对应request header里的range字段，total是文件总大小，不是返回的数据长度 */
    public static final String CONTENT_RANGE_PATTERN = "bytes %d-%d/%d";
    public static final String BYTE_RANGE_PREFIX = "bytes=";

    private DownloadUtil() {
    }

    /** 获取 range 请求头 */
    public static String rangeHeader(HttpServletRequest request) {
        return request.getHeader("Range");
    }



    public static void download(File file, HttpServletRequest request, HttpServletResponse response) throws FileNotFoundException {
        download(Boolean.FALSE, file, request, response);
    }

    public static void download(boolean isAttachment, File file, HttpServletRequest request, HttpServletResponse response) throws FileNotFoundException {
        String contentType = null;
        try {
            contentType = Files.probeContentType(file.getAbsoluteFile().toPath());
        } catch (Exception ex) {
            log.warn("get file content-type fail. {}", ex.getMessage());
        }
        if (contentType == null) {
            contentType = "application/octet-stream";
        }
        download(isAttachment, contentType, file, request, response);
    }

    public static void download(String contentType, File file, HttpServletRequest request, HttpServletResponse response) throws FileNotFoundException {
        download(Boolean.FALSE, contentType, file, request, response);
    }

    public static void download(boolean isAttachment, String contentType, File file, HttpServletRequest request, HttpServletResponse response) throws FileNotFoundException {
        String rangeHeader = rangeHeader(request);
        long fileLength = file.length();
        String fileDownloadName = file.getName();
        InputStream inputStream = new FileInputStream(file);
        download(isAttachment, contentType, fileLength, fileDownloadName, rangeHeader, inputStream, response);
    }


    /**
     * 下载文件
     *
     * @param isAttachment true-表示浏览器直接使用 false-下载成文件
     * @param contentType 文件的 content-type
     * @param fileLength 文件的大小， 单位 byte
     * @param fileDownloadName 下载的文件的名字
     * @param inputStream 文件流
     * @param request {@link HttpServletRequest}
     * @param response {@link HttpServletResponse}
     *
     * @author tarofang@163.com
     * @date 2021年08月05日
     */
    public static void download(boolean isAttachment, String contentType, long fileLength, String fileDownloadName, InputStream inputStream, //
                                HttpServletRequest request, HttpServletResponse response) {
        String rangeHeader = rangeHeader(request);
        download(isAttachment, contentType, fileLength, fileDownloadName, rangeHeader, inputStream, response);
    }

    public static void download(String contentType, long fileLength, String fileDownloadName, InputStream inputStream, //
                                HttpServletRequest request, HttpServletResponse response) {
        download(Boolean.FALSE, contentType, fileLength, fileDownloadName, inputStream, request, response);
    }


    /**
     * 下载文件
     *
     * @param isAttachment true-表示浏览器直接使用 false-下载成文件
     * @param contentType 文件的 content-type
     * @param fileLength 文件的大小， 单位 byte
     * @param fileDownloadName 下载的文件的名字
     * @param rangeHeader 请求头 range 的值， 用于判定是否是分段下载
     * @param inputStream 文件流
     * @param response {@link HttpServletResponse}
     *
     * @author tarofang@163.com
     * @date 2021年08月05日
     */
    public static void download(boolean isAttachment, String contentType, long fileLength, String fileDownloadName, String rangeHeader, //
                                InputStream inputStream, HttpServletResponse response) {


        // String fileName = new String(fileDownloadName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
        // // inline 表示浏览器直接使用，attachment 表示下载，fileName 表示下载的文件名
        // if (isAttachment) {
        //     response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
        // } else {
        //     response.setHeader("Content-Disposition", "inline;filename=" + fileName);
        // }
        //
        // response.setContentType(contentType);
        //
        // final String range = StringUtils.trimToNull(rangeHeader);
        // // 分段下载
        // if (range != null && range.startsWith(BYTE_RANGE_PREFIX)) {
        //     rangeDownload(range, fileLength, response, inputStream);
        //     return;
        // }
        // // 非断点下载， 全量下载
        // fullDownload(fileLength, response, inputStream);
        download(isAttachment, contentType, fileLength, fileDownloadName, rangeHeader, inputStream, response, null);
    }

    public static void download(boolean isAttachment, String contentType, long fileLength, String fileDownloadName, String rangeHeader, //
                                InputStream inputStream, HttpServletResponse response, HttpServletRequest request) {

        String fileName;
        if (request != null) {
            String userAgent = StringUtils.trimToEmpty(request.getHeader("user-agent"));
            userAgent = userAgent.toLowerCase();
            // win10 ie edge 浏览器 和其他系统的ie
            if (userAgent.contains("msie") || userAgent.contains("like gecko")) {
                fileName = UrlEncodeUtil.urlEncode(fileDownloadName);
            } else {
                fileName = new String(fileDownloadName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
            }
        } else {
            fileName = new String(fileDownloadName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
        }

        // inline 表示浏览器直接使用，attachment 表示下载，fileName 表示下载的文件名
        if (isAttachment) {
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
        } else {
            response.setHeader("Content-Disposition", "inline;filename=" + fileName);
        }

        response.setContentType(contentType);

        final String range = StringUtils.trimToNull(rangeHeader);
        // 分段下载
        if (range != null && range.startsWith(BYTE_RANGE_PREFIX)) {
            rangeDownload(range, fileLength, response, inputStream);
            return;
        }
        // 非断点下载， 全量下载
        fullDownload(fileLength, response, inputStream);
    }

    public static void download(String contentType, long fileLength, String fileDownloadName, String rangeHeader, //
                                InputStream inputStream, HttpServletResponse response) {
        download(Boolean.FALSE, contentType, fileLength, fileDownloadName, rangeHeader, inputStream, response);
    }

    /**
     * 分段下载
     *
     * @param range range 请求头  byte=0-11111
     * @param fileLength 文件总大小 byte
     * @param response {@link HttpServletResponse}
     * @param inputStream 文件流
     */
    public static void rangeDownload(String range, long fileLength, HttpServletResponse response, InputStream inputStream) {
        // bytes=0-1
        // bytes=0-
        // bytes=0-113,117-219 -> 多区间方式暂不处理
        // range = range.substring(BYTE_RANGE_PREFIX.length());
        if (StringUtils.contains(range, ",")) {
            log.warn("Unsupported range. [range={}]", range);
            responseErr(response);
            return;
        }

        String[] rangeArr = StringUtils.split(range, "=");
        long start = 0L, end = 0L;
        if (ArrayUtils.getLength(rangeArr) > 1) {
            String[] rangeDataArr = StringUtils.splitByWholeSeparator(rangeArr[1], "-");
            start = BasicParserUtils.parseLong(rangeDataArr[0], 0L);
            end = BasicParserUtils.parseLong(rangeDataArr[1], 0L);
        }
        long rangeEnd = fileLength - 1;
        if (end == 0 || end > rangeEnd) {
            end = rangeEnd;
        }

        // 开始位置 大于 结束位置 -> 服务器不能响应错误的请求的字节范围 -> 非法请求或错误的请求
        if (start > end) {
            log.warn("Invalid range. [range={}]", range);
            responseErr(response);
            return;
        }

        // 响应的 Content-Range 范围
        String contentRange = String.format(CONTENT_RANGE_PATTERN, start, end, fileLength);
        // 响应的 Content-Range 范围, 请求的字节长度
        long contentLength = end - start + 1;
        // 设置范围请求头
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); //206
        response.setHeader("Content-Range", contentRange);
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("Content-Length", String.valueOf(contentLength));

        execDownload(Boolean.TRUE, start, end, response, inputStream);
    }

    public static void responseErr(HttpServletResponse response) {
        try {
            response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); //416
        } catch (Exception ex) {
            log.warn("{}", ex.getMessage());
        }
    }

    /**
     * 全量下载
     *
     * @param fileLength 文件总大小 byte
     * @param response {@link HttpServletResponse}
     * @param inputStream 文件流
     */
    public static void fullDownload(long fileLength, HttpServletResponse response, InputStream inputStream) {
        response.setHeader("Content-Length", String.valueOf(fileLength));
        execDownload(Boolean.FALSE, 0L, 0L, response, inputStream);
    }

    /**
     * 将文件流写入到响应流中
     *
     * @param isRange true-分段下载 false-全量下载
     * @param start 起始位置
     * @param end 结束位置
     * @param response {@link HttpServletResponse}
     * @param inputStream 文件流
     */
    public static void execDownload(boolean isRange, long start, long end, HttpServletResponse response, InputStream inputStream) {
        try (ServletOutputStream outputStream = response.getOutputStream()) {
            write(isRange, start, end, inputStream, outputStream);
            outputStream.flush();
        } catch (ClientAbortException cae) {
            // 服务器端向已经断开的客户端连接写数据, 无需处理，打印日志即可
            if (log.isWarnEnabled()) {
                log.warn("{}", cae.getMessage());
            }
            if (log.isTraceEnabled()) {
                log.trace("{}", cae.getMessage(), cae);
            }
        } catch (Exception ex) {
            log.error("{}", ex.getMessage(), ex);
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                log.error("{}", e.getMessage(), e);
            }
        }
    }

    public static void write(boolean isRange, long start, long end, InputStream inputStream, OutputStream outputStream) throws IOException {
        int readBytes = 1024 * 4;
        byte[] buffer = new byte[readBytes];

        // 全量下载
        if (!isRange) {
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
            return;
        }

        // 分段下载
        if (start != 0) {
            long skip = inputStream.skip(start);
            log.debug("skip bytes [{}].", skip);
        }

        int len = -1;
        long current = start;
        // 读 readBytes  的整数倍部分
        while (current < (end + 1 - readBytes)) {
            len = inputStream.read(buffer);
            current += len;
            outputStream.write(buffer, 0, len);
        }
        // 读取剩下的部分
        if (current < end) {
            len = inputStream.read(buffer, 0, (int) (end + 1 - current));
            outputStream.write(buffer, 0, len);
        }
    }


    public static boolean isRangeDownload(HttpServletRequest request) {
        String rangeHeader = rangeHeader(request);
        final String range = StringUtils.trimToNull(rangeHeader);
        if (range == null || !range.startsWith(BYTE_RANGE_PREFIX)) {
            log.debug("Not a range request. [range={}]", range);
            return false;
        }
        if (StringUtils.contains(range, ",")) {
            log.warn("Invalid range. [range={}]", range);
            return false;
        }
        return true;
    }

    public static void writeWithStreamClose(InputStream inputStream, HttpServletResponse response) {
        try (ServletOutputStream outputStream = response.getOutputStream()) {
            write(inputStream, outputStream);
        } catch (ClientAbortException cae) {
            // 服务器端向已经断开的客户端连接写数据, 无需处理，打印日志即可
            if (log.isWarnEnabled()) {
                log.warn("{}", cae.getMessage());
            }
            if (log.isTraceEnabled()) {
                log.trace("{}", cae.getMessage(), cae);
            }
        } catch (Exception ex) {
            log.error("{}", ex.getMessage(), ex);
        }finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (Exception ex) {
                log.error("{}", ex.getMessage(), ex);
            }
        }
    }

    public static void write(InputStream inputStream, OutputStream outputStream) throws IOException {
        int readBytes = 1024 * 4;
        byte[] buffer = new byte[readBytes];
        int len;
        while ((len = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, len);
        }
        outputStream.flush();

    }


    // public static void obsRangeDownload(ObsClient obsClient, String objKey, HttpServletRequest request, HttpServletResponse response) {
    //
    // }

    /**
     * 获取分段下载的范围
     *
     * 只支持单区间，多区间暂不支持
     *
     * bytes=0-1
     * bytes=0-
     * bytes=0-113,117-219 -> 多区间方式暂不处理
     *
     * @param request {@link HttpServletRequest}
     *
     * @return null - 不支持分段下载
     */
    public static Range getRange(HttpServletRequest request, long fileLength) {
        String rangeHeader = rangeHeader(request);
        String range = StringUtils.trimToNull(rangeHeader);
        if (range == null || !range.startsWith(BYTE_RANGE_PREFIX)) {
            return new Range(Boolean.FALSE, Boolean.FALSE, -1L, -1L);
        }

        if (StringUtils.contains(range, ",")) {
            log.warn("Unsupported range. [range={}]", range);
            return new Range(Boolean.TRUE, Boolean.FALSE, -1L, -1L);
        }

        String[] rangeArr = StringUtils.split(range, "=");
        long start = 0L, end = 0L;
        if (ArrayUtils.getLength(rangeArr) > 1) {
            String[] rangeDataArr = StringUtils.splitByWholeSeparator(rangeArr[1], "-");
            start = BasicParserUtils.parseLong(rangeDataArr[0], 0L);
            end = BasicParserUtils.parseLong(rangeDataArr[1], 0L);
        }
        long rangeEnd = fileLength - 1;
        if (end == 0 || end > rangeEnd) {
            end = rangeEnd;
        }

        // 开始位置 大于 结束位置 -> 服务器不能响应错误的请求的字节范围 -> 非法请求或错误的请求
        if (start > end) {
            log.warn("Invalid range. [range={}]", range);
            return new Range(Boolean.TRUE, Boolean.FALSE, start, end);
        }
        return new Range(Boolean.TRUE, Boolean.TRUE, start, end);
    }

    public static class Range {
        /** 当前请求是否支持是下载 */
        private boolean supportRange;
        /** 分段参数是否有效 */
        private boolean valid;
        /** 开始 */
        private long start;
        /** 结束 */
        private long end;

        public Range() {
        }

        public Range(boolean supportRange, boolean valid, long start, long end) {
            this.supportRange = supportRange;
            this.valid = valid;
            this.start = start;
            this.end = end;
        }

        public boolean isSupportRange() {
            return supportRange;
        }

        public void setSupportRange(boolean supportRange) {
            this.supportRange = supportRange;
        }

        public boolean isValid() {
            return valid;
        }

        public void setValid(boolean valid) {
            this.valid = valid;
        }

        public long getStart() {
            return start;
        }

        public void setStart(long start) {
            this.start = start;
        }

        public long getEnd() {
            return end;
        }

        public void setEnd(long end) {
            this.end = end;
        }
    }
}
